package ae.pcfc.cldos.online.common.dao;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

import org.hibernate.Criteria;
import org.hibernate.FlushMode;
import org.hibernate.Query;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import ae.pcfc.cldos.online.common.domain.BaseEntity;

/**
 * This class will act as a Generic DAO containing all the methods for data access,
 * initially it contains all the necessary methods, later on team will remove the 
 * methods which are not needed in the project
 * 
 * @author arosa
 * @Reviewed By Ammar
 * @Date 25-Jan-2012
 *
 */
@Repository("genericDao")
public abstract class GenericDAO extends BaseDAO {

	private static final int ORACLE_IN_CONDITION_PARAMS_LIMIT = 100;

	/**
	 * Get entity onject by id
	 * 
	 * @param entityClass
	 * @param id
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T extends BaseEntity> T findById(Class<T> entityClass, Long id) {
		return (T) getCurrentSession().get(entityClass, id);
	}

	/**
	 * Get entity object proxy by id
	 * 
	 * @param entityClass
	 * @param id
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T extends BaseEntity> T loadProxyById(Class<T> entityClass, Integer id) {
		return (T) getCurrentSession().load(entityClass, id);
	}

	/**
	 * Find entity object by property
	 * 
	 * @param entityClass
	 * @param propertyName
	 * @param propertyValue
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T extends BaseEntity> T findByProperty(Class<T> entityClass, String propertyName,
			Object propertyValue) {
		DetachedCriteria c = DetachedCriteria.forClass(entityClass);
		c.add(Property.forName(propertyName).eq(propertyValue));
		return (T) findByCriteria(c);
	}

	/**
	 * Find entity object by DetachedCriteria
	 * 
	 * @param entityClass
	 * @param restriction
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T extends BaseEntity> T findByRestriction(Class<T> entityClass, Criterion restriction) {
		DetachedCriteria c = DetachedCriteria.forClass(entityClass);
		c.add(restriction);
		return (T) findByCriteria(c);
	}

	/**
	 * Find any object by criteria
	 * 
	 * @param criteria
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T extends BaseEntity> T findByCriteria(DetachedCriteria criteria) {
		return (T) criteria.getExecutableCriteria(getCurrentSession()).uniqueResult();
	}

	/**
	 * Get object property by criterion and property name
	 * 
	 * @param entityType
	 * @param rest
	 * @param propertyName
	 * @return
	 */
	protected Object getProperty(Class<?> entityType, Criterion rest, String propertyName) {
		return getCurrentSession().createCriteria(entityType).add(rest)
				.setProjection(Property.forName(propertyName)).uniqueResult();
	}

	/**
	 * Get properties list
	 * 
	 * @param entityType
	 * @param rest
	 * @param properties
	 * @return
	 */
	protected List<?> getProperties(Class<?> entityType, Criterion rest, ProjectionList properties) {
		Criteria criteria = getCurrentSession().createCriteria(entityType);
		criteria.add(rest);
		criteria.setProjection(properties);
		return criteria.list();
	}

	/**
	 * Get list by criteria
	 * 
	 * @param criteria
	 * @return
	 */
	protected List<?> getListByCriteria(DetachedCriteria criteria) {
		return criteria.getExecutableCriteria(getCurrentSession()).list();
	}

	/**
	 * Get list by criteria with limits of returned rows
	 * 
	 * @param criteria
	 * @param offset
	 * @param maxResults
	 * @return
	 */
	protected List<?> getListByCriteria(DetachedCriteria criteria, Integer offset, Integer maxResults) {
		Criteria executableCriteria = criteria.getExecutableCriteria(getCurrentSession());
		if (offset >= 0) {
			executableCriteria.setFirstResult(offset);
		}
		if (maxResults > 0) {
			executableCriteria.setMaxResults(maxResults);
		}
		return executableCriteria.list();
	}

	/**
	 * Get total count of rows by criteria
	 * 
	 * @param criteria
	 * @return
	 */
	protected Integer getTotal(DetachedCriteria criteria) {
		return getTotal(criteria, true);
	}

	/**
	 * Get total count by criteria with using sql projections
	 * 
	 * @param criteria
	 * @param useCountProjection
	 * @return
	 */
	protected Integer getTotal(DetachedCriteria criteria, boolean useCountProjection) {
		if (useCountProjection) {
			/* apply the count(*) projection */
			criteria.setProjection(Projections.rowCount());
		}
		/*
		 * Execute the count(*) query
		 */
		Number total = (Number) criteria.getExecutableCriteria(getCurrentSession()).uniqueResult();

		if (useCountProjection) {
			/* clear the projections */
			criteria.setProjection(null);
			criteria.setResultTransformer(Criteria.ROOT_ENTITY);
		}
		return total.intValue();
	}

	/**
	 * Delete entity from database
	 * 
	 * @param entity
	 */
	protected void delete(BaseEntity entity) {		
		getCurrentSession().setFlushMode(FlushMode.MANUAL);
		getCurrentSession().delete(entity);
		getCurrentSession().flush();
	}

	/**
	 * Delete all of entities from database
	 * 
	 * @param collection
	 */
	protected void deleteAll(Collection<?> collection) {
		getCurrentSession().setFlushMode(FlushMode.MANUAL);
		Session session = getCurrentSession();
		for (Object entity : collection) {
			session.delete(entity);
		}
		getCurrentSession().flush();
	}

	/**
	 * Save entity
	 * 
	 * @param entity
	 */
	protected void saveOrUpdate(BaseEntity entity) {
		getCurrentSession().setFlushMode(FlushMode.MANUAL);
		getCurrentSession().saveOrUpdate(entity);
		getCurrentSession().flush();
	}

	/**
	 * Find entity by example
	 * 
	 * @param example
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T extends BaseEntity> T findByExample(T example) {
		Criteria executableCriteria = getCurrentSession().createCriteria(example.getClass());
		executableCriteria.add(Example.create(example));
		return (T) executableCriteria.uniqueResult();
	}

	/**
	 * Get entities list by filter
	 * 
	 * @param collection
	 * @param filter
	 * @return
	 */
	protected List<?> applyFilter(Collection<?> collection, String filter) {
		return getCurrentSession().createFilter(collection, filter).list();
	}
	
	/**
	 * Get items count by filter
	 * 
	 * @param collection
	 * @return
	 */
	protected Integer applyCountFilter(Collection<?> collection) {
		return applyCountFilter(collection, null);
	}

	/**
	 * 
	 * 
	 * @param collection
	 * @param condition
	 * @return
	 */
	protected Integer applyCountFilter(Collection<?> collection, String condition) {
		String q = "SELECT COUNT(*)";
		if (StringUtils.hasText(condition)) {
			StringBuffer buf = new StringBuffer(q);
			buf.append(" WHERE ");
			buf.append(condition);
			q = buf.toString();
		}
		return (Integer) getCurrentSession().createFilter(collection, q).uniqueResult();
	}

	/**
	 * Update items
	 * 
	 * @param query
	 * @param args
	 * @return
	 */
	protected Integer updateInBatch(String query, Object... args) {
		if (hasNullArgs(args)) {
			query = replaceNullArgs(query, args);
			args = removeNullArgs(args);
		}
		Query queryObject = getCurrentSession().createQuery(query);
		for (int i = 0; i < args.length; i++) {
			queryObject.setParameter(i, args[i]);
		}
		return queryObject.executeUpdate();
	}

	/**
	 * Cleat current hibernate session
	 * 
	 */
	protected void cleanSession() {
		getCurrentSession().clear();
	}

	/**
	 * Flush current hibernate session
	 * 
	 */
	protected void flushSession() {
		getCurrentSession().flush();
	}

	/**
	 * Save not cascade
	 * 
	 * 
	 * @param entity
	 */
	protected void saveNotCascade(BaseEntity entity) {
		StatelessSession statelessSession = factory.openStatelessSession();
		Transaction tx = statelessSession.beginTransaction();
		tx.begin();
		statelessSession.insert(entity);
		tx.commit();
		statelessSession.close();
	}

	/**
	 * Update not cascades
	 * 
	 * @param entity
	 */
	protected void updateNotCascade(BaseEntity entity) {
		StatelessSession statelessSession = factory.openStatelessSession();
		Transaction tx = statelessSession.beginTransaction();
		tx.begin();
		statelessSession.update(entity);
		tx.commit();
		statelessSession.close();
	}

	/**
	 * Execute native SQL with args
	 * 
	 * @param sqlQueryString
	 * @param args
	 */
	protected void executeSQL(String sqlQueryString, Object... args) {
		if (hasNullArgs(args)) {
			sqlQueryString = replaceNullArgs(sqlQueryString, args);
			args = removeNullArgs(args);
		}
		Session session = getCurrentSession();
		SQLQuery query = session.createSQLQuery(sqlQueryString);
		for (Object object : args) {
			query.setParameter(0, object);
		}
		query.executeUpdate();
	}

	/**
	 * Delete not cascade
	 * 
	 * @param entity
	 */
	protected void deleteNotCascade(BaseEntity entity) {
		StatelessSession statelessSession = factory.openStatelessSession();
		Transaction tx = statelessSession.beginTransaction();
		tx.begin();
		statelessSession.delete(entity);
		tx.commit();
		statelessSession.close();
	}

	/**
	 * Get list by HQL query
	 * 
	 * @param hqlQueryString
	 * @param args
	 * @return
	 */
	protected List<?> getListByHqlQuery(String hqlQueryString, Object... args) {
		if (hasNullArgs(args)) {
			hqlQueryString = replaceNullArgs(hqlQueryString, args);
			args = removeNullArgs(args);
		}
		Query queryObject = getCurrentSession().createQuery(hqlQueryString);
		if (args != null) {
			for (int i = 0; i < args.length; i++) {
				queryObject.setParameter(i, args[i]);
			}
		}
		return queryObject.list();
	}

	/**
	 * Get list by native SQL query
	 * 
	 * @param sql
	 * @param params
	 * @return
	 */
	protected List<?> getListBySqlQuery(String sql, Object... params) {
		return getListBySqlQuery(sql, -1, -1, params);
	}

	/**
	 * Get list by natiove SQL query with params
	 * 
	 * @param sql
	 * @param offset
	 * @param maxResults
	 * @param params
	 * @return
	 */
	protected List<?> getListBySqlQuery(String sql, Integer offset, Integer maxResults, Object... params) {
		if (hasNullArgs(params)) {
			sql = replaceNullArgs(sql, params);
			params = removeNullArgs(params);
		}
		Session session = getCurrentSession();
		SQLQuery query = session.createSQLQuery(sql);
		return getListBySqlQuery(query, offset, maxResults, params);
	}

	/**
	 * Get list by native SQL query using SQLQurey object
	 * 
	 * @param sqlq
	 * @param offset
	 * @param maxResults
	 * @param params
	 * @return
	 */
	protected List<?> getListBySqlQuery(Query sqlq, Integer offset, Integer maxResults, Object... params) {

		if (hasNullArgs(params)) {
			params = removeNullArgs(params);
		}
		// Set parameters
		if (params != null) {
			for (int i = 0; i < params.length; i++) {
				sqlq.setParameter(i, params[i]);
			}
		}
		if (offset >= 0) {
			sqlq.setFirstResult(offset);
		}
		if (maxResults > 0) {
			sqlq.setMaxResults(maxResults);
		}
		return sqlq.list();
	}

	/**
	 * Delete items by id-list
	 * 
	 * @param entityType
	 * @param ids
	 */
	protected <T extends BaseEntity> void deleteInBatch(Class<T> entityType, Set<?> ids) {
		List<Object> params = new ArrayList<Object>(ids.size());
		Collections.addAll(params, ids.toArray());
		deleteInBatch(entityType, params);
	}

	/**
	 * Delete with restrictions of count
	 * 
	 * @param entityType
	 * @param ids
	 */
	protected <T extends BaseEntity> void deleteInBatch(Class<T> entityType, List<?> ids) {
		if (!ids.isEmpty()) {

			for (int i = 0; i < ids.size(); i += ORACLE_IN_CONDITION_PARAMS_LIMIT) {

				int deleteLimit = Math.min(ids.size() - i, ORACLE_IN_CONDITION_PARAMS_LIMIT);
				List<?> currentList = ids.subList(i, i + deleteLimit);

				StringBuffer params = new StringBuffer("?");
				for (int j = 0; j < currentList.size() - 1; j++) {
					params.append(",?");
				}
				StringBuffer query = new StringBuffer("delete ");
				query.append(entityType.getSimpleName());
				query.append(" entity where entity.id in (");
				query.append(params.toString());
				query.append(")");

				updateInBatch(query.toString(), currentList.toArray());
			}
		}
	}

	/**
	 * Delete entity by id
	 * 
	 * @param entityType
	 * @param id
	 */
	protected <T extends BaseEntity> void deleteById(Class<T> entityType, Long id) {
		List<Object> idList = new ArrayList<Object>(1);
		idList.add(id);
		deleteInBatch(entityType, idList);
	}

	/**
	 * Get current session
	 * 
	 * @return
	 */
	protected Session getCurrentSession() {
		return factory.getCurrentSession();
	}

	/* Helper methods */

	/**
	 * Returns true if there are null args in array.
	 * 
	 * @param args
	 * @return
	 */
	private boolean hasNullArgs(Object... args) {
		boolean result = false;
		for (Object arg : args) {
			if (arg == null) {
				result = true;
				break;
			}
		}
		return result;
	}

	/**
	 * Replaces "?" whith "NULL" in query string.
	 * 
	 * @param query
	 * @param args
	 * @return
	 */
	private String replaceNullArgs(String query, Object... args) {
		StringBuffer buf = new StringBuffer(query);
		/*
		 * Fix query string, replace null parameters with NULL value in query.
		 */
		int index = buf.lastIndexOf("?");
		for (int count = args.length - 1; index != -1; count--) {
			if (args[count] == null) {
				buf.replace(index, index + 1, "NULL");
			}
			index = buf.lastIndexOf("?", index - 1);
		}
		return buf.toString();
	}

	/**
	 * Removes null-s from array.
	 * 
	 * @param args
	 * @return
	 */
	private Object[] removeNullArgs(Object... args) {
		List<Object> params = new ArrayList<Object>(args.length);
		for (int i = 0; i < args.length; i++) {
			if (args[i] != null) {
				params.add(args[i]);
			}
		}
		return params.toArray();
	}

	/**
	 * Get map list by HQL query
	 * 
	 * @param hqlQueryString
	 * @param args
	 * @return
	 */
	protected List<?> getListMapByHqlQuery(String hqlQueryString, Object... args) {
		if (hasNullArgs(args)) {
			hqlQueryString = replaceNullArgs(hqlQueryString, args);
			args = removeNullArgs(args);
		}
		Query queryObject = getCurrentSession().createQuery(hqlQueryString);
		String[] Aliases = queryObject.getReturnAliases();
		if (args != null) {
			for (int i = 0; i < args.length; i++) {
				queryObject.setParameter(i, args[i]);
			}
		}
		List<HashMap<String, Object>> results = new ArrayList<HashMap<String, Object>>();
		List<?> lpQueryList = queryObject.list();
		for (Object row : lpQueryList) {
			int i = 0;
			 Object[] lpRow = (Object[]) row;
			HashMap<String, Object> lpMap = new HashMap<String, Object>();
			for (Object col : lpRow) {
				lpMap.put(Aliases[i++], col);
			}
			results.add(lpMap);
		}
		return results;
	}
	
	/**
	 * Run named query by named query name
	 * 
	 * @param name
	 */
	protected void runNamedQuery(String name){
		Query query = getCurrentSession().getNamedQuery(name);
		query.executeUpdate();
	}

}
